

/**
 * @author stefan.liebenberg@lfpug.com (Stefan)
 * @fileoverview Creates a class that allows for renderer's
 * and some features from goog.ui.Container without the
 * need to use goog.ui.Control elements as children.
 */

goog.provide('lfpug.ui.Component');
goog.require('goog.functions');
goog.require('goog.ui.Component');
goog.require('goog.ui.registry');
goog.require('lfpug.binding.Binding');
goog.require('lfpug.ui.Renderer');



/**
 * The base component is extended from @code {goog.ui.Component} to provide
 * some of features that @code {goog.ui.Container} does, but without requiring
 * that all it's children are @code {goog.ui.Control} elements.
 *
 * @constructor
 *
 * @extends {goog.ui.Component}
 *
 * @param {lfpug.ui.type.Model=}  opt_model
 *        The adaptor or basemodel.
 *
 * @param {lfpug.ui.Renderer=} opt_renderer
 *        Optional renderer.
 *
 * @param {goog.dom.DomHelper=} opt_domHelper
 *        Optional dom helper.
 */
lfpug.ui.Component =
    function(opt_model, opt_renderer, opt_domHelper) {

  // set renderer.
  if (goog.isDefAndNotNull(opt_renderer)) {
    this.setRenderer(opt_renderer);
  } else {
    this.setDefaultRenderer();
  }

  goog.base(this, opt_domHelper);

  this.bindings_enabled_ = false;
  if (goog.isDefAndNotNull(opt_model)) {
    this.setModel(opt_model);
  }
  /**
   * @private
   * @type {Object.<string, Array.<lfpug.binding.Binding>>}
   * */
  this.bindings_ = {};
};
goog.inherits(lfpug.ui.Component, goog.ui.Component);


/**
 * @private
 * @type {lfpug.ui.Renderer}
 */
lfpug.ui.Component.prototype.renderer_;


/**
 * @private
 * @param {string} event The events being bound.
 * @param {Function} template The templates being bound.
 * @param {string} elementResolver The function which resolves the element
 * being replaced.
 * @param {Function} templateObjFunc The function which builds the template
 * object.
 * @param {Object} scope The scope which the template object function should
 * be called.
 * @param {Array=} opt_args Any arguments you wish to pass to the
 * templateObjFunc.
 * @param {Array.<Function>=} opt_triggers The triggers I want fired when this
 *  binding has executed.
 *  @return {lfpug.binding.Binding} The binding which was created. The only time
 *  to use the return value is if you want to explicitly manage the existance of
 *  a binding.
 */
lfpug.ui.Component.prototype.registerBinding_ =
    function(event, template, elementResolver, templateObjFunc,
             scope, opt_args, opt_triggers) {
  /** @type {Array} */
  var arr = [];
  /** @type {Array} */
  var tarr = [];
  /** @type {lfpug.binding.Binding} */
  var binding = new lfpug.binding.Binding(
      this, event, template, elementResolver,
      templateObjFunc, scope, opt_args, opt_triggers
      );
  arr.push(binding);
  if (goog.isDefAndNotNull(this.bindings_[event])) {
    tarr = this.bindings_[event];
    tarr.splice(tarr.length - 1, 0, binding);
  } else {
    this.bindings_[event] = [binding];
  }
  return binding;
};


/**
 * @private
 * @param {lfpug.model.BaseModel.ChangedProperty} changedProp The event
 *          name which got triggered on the model.
 */
lfpug.ui.Component.prototype.executeBindings_ = function(changedProp) {
  if (goog.isDefAndNotNull(this.bindings_[changedProp.name])) {
    var arr = this.bindings_[changedProp.name];
    /** @type {lfpug.binding.Binding} */
    var binding;
    for (var i = 0, l = arr.length; i < l; i++) {
      binding = arr[i];
      binding.execute(changedProp);
    }
  }
};


/**
 * Sets the Renderer.
 * @param {lfpug.ui.Renderer} renderer
 *        The renderer for this component.
 */
lfpug.ui.Component.prototype.setRenderer =
    function(renderer) {
  var element = this.getElement();

  // if the dom is already created, a new renderer cannot be set.
  if (goog.isDefAndNotNull(element)) {
    throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);
  }

  this.renderer_ = renderer;
};


/**
 * Sets the default renderer for this component.
 */
lfpug.ui.Component.prototype.setDefaultRenderer =
    function() {
  var renderer = lfpug.ui.Renderer.getInstance();
  this.setRenderer(renderer);
};


/** @override */
lfpug.ui.Component.prototype.createDom = function() {
  this.setElementInternal(this.renderer_.createDom(this));
};


/** @override */
lfpug.ui.Component.prototype.getContentElement =
    function() {
  return this.renderer_.getContentElement(this.getElement());
};


/**
 * @return {lfpug.ui.Renderer}
 *         Renderer for this Component.
 */
lfpug.ui.Component.prototype.getRenderer = function() {
  return this.renderer_;
};


/**
 * @param {Element} element
 *        The element object.
 * @return {boolean}
 *         Returns true if the element can be decorated.
 */
lfpug.ui.Component.prototype.canDecorate = function(element) {
  return this.renderer_.canDecorate(element);
};


/**
 * @param {Element} element
 *        Element to decorate.
 */
lfpug.ui.Component.prototype.decorateInternal =
    function(element) {
  this.setElementInternal(this.renderer_.decorate(this, element));
};


/**
 * TODO(Stefan) figure out if calling base()
 *              here and then loadModule is bug free.
 *
 * @override */
lfpug.ui.Component.prototype.enterDocument =
    function() {
  goog.base(this, 'enterDocument');
  this.loadModel();
};


/**
 * @private
 * @type {boolean}
 */
lfpug.ui.Component.prototype.bindings_enabled_;


/**
 * Sets up the bindings.
 * @protected
 */
lfpug.ui.Component.prototype.setupBindings = function() {
  if (!this.bindings_enabled_ && this.isInDocument()) {
    if (goog.isDefAndNotNull(this.model_)) {
      this.setupBindingsInternal();
      this.bindings_enabled_ = true;
    }
  }
};


/**
 * @protected
 */
lfpug.ui.Component.prototype.teardownBindings =
    function() {
  if (this.bindings_enabled_) {
    if (goog.isDefAndNotNull(this.model_)) {
      this.teardownBindingsInternal();
    }
    this.bindings_enabled_ = false;
  }
};


/**
 * @protected
 */
lfpug.ui.Component.prototype.setupBindingsInternal =
    function() {
  // do binding in this function
};


/** @override */
lfpug.ui.Component.prototype.exitDocument =
    function() {
  this.unloadModel();
  goog.base(this, 'exitDocument');
};


/**
 * @protected
 */
lfpug.ui.Component.prototype.teardownBindingsInternal =
    function() {
  // destroy binding in this function.
};


/**
 * @private
 * @type {lfpug.ui.type.Model}
 */
lfpug.ui.Component.prototype.model_;


/**
 * @param {lfpug.ui.type.Model} model
 *        The model associated with this Component.
 */
lfpug.ui.Component.prototype.setModel = function(model) {
  this.unloadModel();
  if (goog.isDefAndNotNull(model)) {
    this.model_ = model;
    this.loadModel();
  } else {
    this.model_ = null;
  }
};


/**
 * @return {lfpug.ui.type.Model}
 *        The model associated with this Component.
 */
lfpug.ui.Component.prototype.getModel = function() {
  return this.model_;
};


/**
 * Function to manage model in the Component.
 * @protected
 */
lfpug.ui.Component.prototype.loadModel = function() {
  var model = this.getModel();
  if (goog.isDefAndNotNull(model)) {
    this.loadModelInternal();
    if (!this.bindings_enabled_) {
      this.setupBindings();
    }
  }
};


/**
 * Function to manage model in the Component.
 * @protected
 */
lfpug.ui.Component.prototype.loadModelInternal =
    function() {
  this.loadModelItems();
};


/**
 * Function to manage model in the Component.
 * @protected
 */
lfpug.ui.Component.prototype.unloadModel = function() {
  var model = this.getModel();
  if (goog.isDefAndNotNull(model)) {
    this.unloadModelInternal();
    this.teardownBindings();
  }
};


/**
 * Remove all model elements from the component in this function.
 * @protected
 */
lfpug.ui.Component.prototype.unloadModelInternal =
    function() {
  this.unloadModelItems();
};


/**
 * Load the child items from our model.
 */
lfpug.ui.Component.prototype.loadModelItems = function() {
  var model = this.getModel();
  if (goog.isDefAndNotNull(model.getLength)) {
    goog.array.forEach(model.getSource(), this.addChildModel, this);
  }
};


/**
 * Load the child items from our model.
 */
lfpug.ui.Component.prototype.unloadModelItems = function() {
  var model = this.getModel();
  if (goog.isDefAndNotNull(model.getLength)) {
    goog.array.forEach(model.getSource(), this.removeChildModel, this);
  }
};


/**
 * @param {lfpug.ui.type.Model} model
 *        The model to search for.
 *
 * @return {goog.ui.Component}
 *         The found component.
 */
lfpug.ui.Component.prototype.getChildWithModel =
    function(model) {
  /** @type {number} */
  var length = this.getChildCount();
  for (var index = 0; index < length; index++) {
    /** @type {goog.ui.Component} */
    var child = this.getChildAt(index);
    if (child.getModel() === model) {
      return child;
    }
  }
  return null;
};


/**
 * @param {lfpug.ui.type.Model} model
 *        The model to build child from.
 */
lfpug.ui.Component.prototype.addChildModel = function(model) {
  var count = this.getChildCount();
  this.addChildModelAt(model, count);
};


/**
 * @param {lfpug.ui.type.Model} model
 *        The model element.
 *
 * @param {number} index
 *        The Child index.
 */
lfpug.ui.Component.prototype.addChildModelAt =
    function(model, index) {

  /** @type {goog.ui.Component} */
  var child = this.getChildWithModel(model);

  // if child already exists, just move it to propper index.
  if (goog.isDefAndNotNull(child)) {
    this.addChildAt(child, index, false);
  } else {
    /** @type {lfpug.ui.Component} */
    var new_child = this.getItemComponent(model);
    if (goog.isDefAndNotNull(new_child)) {
      this.addChildAt(new_child, index, true);
    }
  }
};


/**
 *
 * Remove child that has this model.
 * @param {lfpug.ui.type.Model} model
 *        The model associated with child.
 */
lfpug.ui.Component.prototype.removeChildModel =
    function(model) {
  var child = this.getChildWithModel(model);
  if (goog.isDefAndNotNull(child)) {
    this.removeChild(child, true);
    child.dispose();
  }
};


/**
 * @type {lfpug.ui.ComponentFactory}
 */
lfpug.ui.Component.factory = new lfpug.ui.ComponentFactory();


/**
 * @return {boolean}
 *         True if component factory is associated.
 */
lfpug.ui.Component.prototype.hasAssociatedComponentFactory =
    goog.functions.FALSE;


/**
 * @return {lfpug.ui.ComponentFactory}
 *         The component factory.
 */
lfpug.ui.Component.prototype.getAssociatedComponentFactory =
    goog.functions.constant(lfpug.ui.ComponentFactory.factory);


/**
 *
 * @param {lfpug.ui.type.Model} item
 *        An item to add.
 *
 * @return {lfpug.ui.Component}
 *         A item generated from the factories.
 */
lfpug.ui.Component.prototype.getItemComponent = function(item) {
  var component, factory;
  if (this.hasAssociatedComponentFactory()) {
    factory = this.getAssociatedComponentFactory();
    component = factory.create(item);
    if (goog.isDefAndNotNull(component)) {
      return /** @type {lfpug.ui.Component} */(component);
    }
    return null;
  }

  var model = this.getModel();
  // this if ignores PM's and AC's.
  /**
   * TODO note this is an implementation coupled to how we resolve factories
   * I would recommend something similiar, but it is out of scope for this talk
   */
  //  if (model instanceof lfpug.ui.PresentationModel) {
  //    if (model.hasAssociatedComponentFactory()) {
  //      factory = this.getAssociatedComponentFactory();
  //      component = factory.create(item);
  //      if (goog.isDefAndNotNull(component)) {
  //        return component;
  //      }
  //    }
  //  }

  factory = lfpug.ui.ComponentFactory.getGlobalFactory();
  return /** @type {lfpug.ui.Component} */ (factory.create(item));
};


/**
 * @override
 */
lfpug.ui.Component.prototype.disposeInternal =
    function() {
  this.setModel(null);
  goog.base(this, 'disposeInternal');
};




// Register a decorator factory function for
// lfpug.ui.Component
goog.ui.registry.setDecoratorByClassName(
    lfpug.ui.Renderer.CSS_CLASS,
    function() {
      return new lfpug.ui.Component(null);
    });
